using System;
using System.Collections;
using System.Collections.Generic;
using Velocity4Net.Runtime.Parse.AST;
using Velocity4Net.Errors;
using Velocity4Net.Runtime.Directives;

namespace Velocity4Net.Runtime
{
    /**
     * Manages VMs in namespaces.  Currently, two namespace modes are
     * supported:
     *
     * <ul>
     * <li>flat - all allowable VMs are in the global namespace</li>
     * <li>local - inline VMs are added to it's own template namespace</li>
     * </ul>
     *
     * Thanks to <a href="mailto:JFernandez@viquity.com">Jose Alberto Fernandez</a>
     * for some ideas incorporated here.
     *
     * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
     * @author <a href="mailto:JFernandez@viquity.com">Jose Alberto Fernandez</a>
     * @version $Id: VelocimacroManager.java 992115 2010-09-02 21:00:10Z nbubna $
     */
    public class VelocimacroManager
    {
        private static String GLOBAL_NAMESPACE = "";

        private bool registerFromLib = false;

        /** Hash of namespace hashes. */
        private IDictionary namespaceHash = new Hashtable();

        /** reference to global namespace hash */
        private IDictionary globalNamespace;

        /** set of names of library tempates/namespaces */
        private ISet<object> libraries = new System.Collections.Generic.HashSet<object>();

        private RuntimeServices rsvc = null;

        /*
         * big switch for namespaces.  If true, then properties control
         * usage. If false, no.
         */
        private bool namespacesOn = true;
        private bool inlineLocalMode = false;
        private bool inlineReplacesGlobal = false;

        /**
         * Adds the global namespace to the hash.
         */
        public VelocimacroManager(RuntimeServices rsvc)
        {
            /*
             *  add the global namespace to the namespace hash. We always have that.
             */

            globalNamespace = addNamespace(GLOBAL_NAMESPACE);
            this.rsvc = rsvc;
        }

        /**
         * Adds a VM definition to the cache.
         * 
         * Called by VelocimacroFactory.addVelociMacro (after parsing and discovery in Macro directive)
         * 
         * @param vmName Name of the new VelociMacro.
         * @param macroBody String representation of the macro body.
         * @param argArray Array of macro parameters, first parameter is the macro name.
         * @param namespace The namespace/template from which this macro has been loaded.
         * @return Whether everything went okay.
         */
        public bool addVM(String vmName, INode macroBody, String[] argArray,
                             String ns, bool canReplaceGlobalMacro)
        {
            if (macroBody == null)
            {
                // happens only if someone uses this class without the Macro directive
                // and provides a null value as an argument
                throw new VelocityException("Null AST for " + vmName + " in " + ns);
            }

            MacroEntry me = new MacroEntry(vmName, macroBody, argArray, ns, rsvc);

            me.setFromLibrary(registerFromLib);

            /*
             *  the client (VMFactory) will signal to us via
             *  registerFromLib that we are in startup mode registering
             *  new VMs from libraries.  Therefore, we want to
             *  addto the library IDictionary for subsequent auto reloads
             */

            bool isLib = true;

            MacroEntry exist = (MacroEntry)globalNamespace[vmName];

            if (registerFromLib)
            {
                libraries.Add(ns);
            }
            else
            {
                /*
                 *  now, we first want to check to see if this namespace (template)
                 *  is actually a library - if so, we need to use the global namespace
                 *  we don't have to do this when registering, as namespaces should
                 *  be shut off. If not, the default value is true, so we still go
                 *  global
                 */

                isLib = libraries.Contains(ns);
            }

            if (!isLib && usingNamespaces(ns))
            {
                /*
                 *  first, do we have a namespace hash already for this namespace?
                 *  if not, add it to the namespaces, and add the VM
                 */

                IDictionary local = getNamespace(ns, true);
                local.Add(vmName, me);

                return true;
            }
            else
            {
                /*
                 *  otherwise, add to global template.  First, check if we
                 *  already have it to preserve some of the autoload information
                 */


                if (exist != null)
                {
                    me.setFromLibrary(exist.getFromLibrary());
                }

                /*
                 *  now add it
                 */

                globalNamespace.Add(vmName, me);

                return true;
            }
        }

        /**
         * Gets a VelocimacroProxy object by the name / source template duple.
         * 
         * @param vmName Name of the VelocityMacro to look up.
         * @param namespace Namespace in which to look up the macro.
         * @return A proxy representing the Macro.
         */
        public VelocimacroProxy get(String vmName, String ns)
        {
            return (get(vmName, ns, null));
        }

        /**
         * Gets a VelocimacroProxy object by the name / source template duple.
         * 
         * @param vmName Name of the VelocityMacro to look up.
         * @param namespace Namespace in which to look up the macro.
         * @param renderingTemplate Name of the template we are currently rendering.
         * @return A proxy representing the Macro.
         * @since 1.6
         */
        public VelocimacroProxy get(String vmName, String ns, String renderingTemplate)
        {
            if (inlineReplacesGlobal && renderingTemplate != null)
            {
                /*
                 * if VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL is true (local macros can
                 * override global macros) and we know which template we are rendering at the
                 * moment, check if local namespace contains a macro we are looking for
                 * if so, return it instead of the global one
                 */
                IDictionary local = getNamespace(renderingTemplate, false);
                if (local != null)
                {
                    MacroEntry me = (MacroEntry)local[vmName];

                    if (me != null)
                    {
                        return me.getProxy(ns);
                    }
                }
            }

            if (usingNamespaces(ns))
            {
                IDictionary local = getNamespace(ns, false);

                /*
                 *  if we have macros defined for this template
                 */

                if (local != null)
                {
                    MacroEntry me = (MacroEntry)local[vmName];

                    if (me != null)
                    {
                        return me.getProxy(ns);
                    }
                }
            }

            /*
             * if we didn't return from there, we need to simply see
             * if it's in the global namespace
             */

            MacroEntry me1 = (MacroEntry)globalNamespace[vmName];

            if (me1 != null)
            {
                return me1.getProxy(ns);
            }

            return null;
        }

        /**
         * Removes the VMs and the namespace from the manager.
         * Used when a template is reloaded to avoid
         * losing memory.
         *
         * @param namespace namespace to dump
         * @return bool representing success
         */
        public bool dumpNamespace(String ns)
        {
            if (usingNamespaces(ns))
            {
                lock (this)
                {
                    IDictionary h = (IDictionary)namespaceHash[ns];
                    namespaceHash.Remove(ns);
                    if (h == null)
                    {
                        return false;
                    }

                    h.Clear();

                    return true;
                }
            }
            return false;
        }

        /**
         *  public switch to let external user of manager to control namespace
         *  usage indep of properties.  That way, for example, at startup the
         *  library files are loaded into global namespace
         *
         * @param namespaceOn True if namespaces should be used.
         */
        public void setNamespaceUsage(bool namespaceOn)
        {
            this.namespacesOn = namespaceOn;
        }

        /**
         * Should macros registered from Libraries be marked special?
         * @param registerFromLib True if macros from Libs should be marked.
         */
        public void setRegisterFromLib(bool registerFromLib)
        {
            this.registerFromLib = registerFromLib;
        }

        /**
         * Should macros from the same template be inlined?
         *
         * @param inlineLocalMode True if macros should be inlined on the same template.
         */
        public void setTemplateLocalInlineVM(bool inlineLocalMode)
        {
            this.inlineLocalMode = inlineLocalMode;
        }

        /**
         *  returns the hash for the specified namespace, and if it doesn't exist
         *  will create a new one and add it to the namespaces
         *
         *  @param namespace  name of the namespace :)
         *  @param addIfNew  flag to add a new namespace if it doesn't exist
         *  @return namespace IDictionary of VMs or null if doesn't exist
         */
        private IDictionary getNamespace(String ns, bool addIfNew)
        {
            IDictionary h = (IDictionary)namespaceHash[ns];

            if (h == null && addIfNew)
            {
                h = addNamespace(ns);
            }

            return h;
        }

        /**
         *   adds a namespace to the namespaces
         *
         *  @param namespace name of namespace to add
         *  @return Hash added to namespaces, ready for use
         */
        private IDictionary addNamespace(String ns)
        {
            IDictionary h = new Hashtable();

            namespaceHash.Add(ns, h);

            return h;
        }

        /**
         *  determines if currently using namespaces.
         *
         *  @param namespace currently ignored
         *  @return true if using namespaces, false if not
         */
        private bool usingNamespaces(String ns)
        {
            /*
             *  if the big switch turns of namespaces, then ignore the rules
             */

            if (!namespacesOn)
            {
                return false;
            }

            /*
             *  currently, we only support the local template namespace idea
             */

            if (inlineLocalMode)
            {
                return true;
            }

            return false;
        }

        /**
         * Return the library name for a given macro.
         * @param vmName Name of the Macro to look up.
         * @param namespace Namespace to look the macro up.
         * @return The name of the library which registered this macro in a namespace.
         */
        public String getLibraryName(String vmName, String ns)
        {
            if (usingNamespaces(ns))
            {
                IDictionary local = getNamespace(ns, false);

                /*
                 *  if we have this macro defined in this namespace, then
                 *  it is masking the global, library-based one, so
                 *  just return null
                 */

                if (local != null)
                {
                    MacroEntry me = (MacroEntry)local[vmName];

                    if (me != null)
                    {
                        return null;
                    }
                }
            }

            /*
             * if we didn't return from there, we need to simply see
             * if it's in the global namespace
             */

            MacroEntry me1 = (MacroEntry)globalNamespace[vmName];

            if (me1 != null)
            {
                return me1.getSourceTemplate();
            }

            return null;
        }

        /**
         * @since 1.6
         */
        public void setInlineReplacesGlobal(bool _is)
        {
            inlineReplacesGlobal = _is;
        }


        /**
         *  wrapper class for holding VM information
         */
        internal class MacroEntry
        {
            private String vmName;
            private String[] argArray;
            private String sourceTemplate;
            private SimpleNode nodeTree = null;
            private bool fromLibrary = false;
            private VelocimacroProxy vp;

            public MacroEntry(String vmName, INode macro,
                       String[] argArray, String sourceTemplate,
                       RuntimeServices rsvc)
            {
                this.vmName = vmName;
                this.argArray = argArray;
                this.nodeTree = (SimpleNode)macro;
                this.sourceTemplate = sourceTemplate;

                vp = new VelocimacroProxy();
                vp.Name = (this.vmName);
                vp.ArgArray = (this.argArray);
                vp.NodeTree = (this.nodeTree);
                vp.setLocation(macro.Line, macro.Column, macro.TemplateName);
                vp.init(rsvc);
            }

            /**
             * Has the macro been registered from a library.
             * @param fromLibrary True if the macro was registered from a Library.
             */
            public void setFromLibrary(bool fromLibrary)
            {
                this.fromLibrary = fromLibrary;
            }

            /**
             * Returns true if the macro was registered from a library.
             * @return True if the macro was registered from a library.
             */
            public bool getFromLibrary()
            {
                return fromLibrary;
            }

            /**
             * Returns the node tree for this macro.
             * @return The node tree for this macro.
             */
            public SimpleNode getNodeTree()
            {
                return nodeTree;
            }

            /**
             * Returns the source template name for this macro.
             * @return The source template name for this macro.
             */
            public String getSourceTemplate()
            {
                return sourceTemplate;
            }

            public VelocimacroProxy getProxy(String ns)
            {
                /*
                 * FIXME: namespace data is omitted, this probably 
                 * breaks some error reporting?
                 */
                return vp;
            }
        }
    }


}
